add built-in herdr integration - #360
Conversation
47cb319 to
91223df
Compare
2ab79ce to
9327ae8
Compare
b65debe to
0776578
Compare
Herdr cannot detect Prime Agent panes: its process detection only knows the legacy "pi" binary name, and its "herdr integration install pi" writes to ~/.pi/agent/extensions which Prime Agent does not read. Ship the pi lifecycle-reporter as a built-in extension factory loaded for every session. It reports working/idle/blocked and session refs to Herdr's unix socket and releases the pane on shutdown, using the agent label "prime-agent" (verified accepted and displayed by herdr 0.7.0 against a live pane). The factory captures HERDR_* env per invocation, so daemon sessions pick up their own pane identity inside the client-env window, and it is a no-op outside Herdr or when the user has installed Herdr's own file-based integration.
…se only on quit Herdr guards pane.report_agent with a per-source monotonic seq and silently drops lower-seq reports (verified live: ok response, state unchanged). Seeding seq per factory instance meant a successor session after /new, resume, fork, or reload could restart below the predecessor's counter, so all its reports were dropped and the pane stuck at working. - move the seq counter to module scope and clamp it to max(prev + 1, now*1000) so it never regresses across instances - release the pane only on quit; on session replacement the successor re-reports immediately and a racing release could clear it - report idle immediately on agent_end when no messages are queued; keep the debounce only when queued follow-ups restart the loop
- unsubscribe the shared-bus herdr:blocked listener on session_shutdown; the event bus survives reloads and session replacements, so a leaked listener kept stale instances reporting with captured pane identity - move the file-based integration check into createAgentSessionServices using the same extension dirs the loader discovers from, so agentDir overrides are honored - stop treating the legacy ~/.pi/agent path as an active reporter; Prime Agent never loads extensions from there, so deferring to it left panes with no reporter at all
…k deferral on reload - bind the reporter to the first session that starts and ignore events from other sessions sharing the closure; inline RLM children reuse the parent's resource loader, so a subagent's turns flipped the pane state and a subagent quit released the pane while the parent was running - skip the built-in reporter entirely for subagent runtimes (rlmDepth > 0), which share the parent's HERDR_* pane identity - re-check for Herdr's file-based integration on every factory invocation instead of once at service creation, so installing it and running /reload hands the pane over without both reporters active - also match herdr-agent-state.js for compiled installs
releaseAgent now stops new reports, drops the queued state, and awaits the in-flight drain before sending pane.release_agent, so a late report cannot land after the release and reclaim the pane for an exited agent.
Daemon-driven reloads and extension ctx.reload() are not gated on idle, so a fresh reporter's session_start could publish idle while the agent was still streaming, and the agentActive guard then swallowed the turn's real end transition. session_start now seeds agentActive from ctx.isIdle().
… its timer A herdr:blocked event during the retry-hold window cancelled the retry timer but left retryHoldActive set, so after the block lifted the pane reported working forever. The blocked handler now settles the hold to failureBlocked, matching what the cancelled timer would have done.
0776578 to
cb6d8f7
Compare
…Extensions With noExtensions the loader never scans the extension dirs, so a file-based herdr-agent-state there never becomes an active reporter; deferring to it left the pane with no reporter at all.
| let idleTimer: ReturnType<typeof setTimeout> | undefined; | ||
| let retryTimer: ReturnType<typeof setTimeout> | undefined; | ||
|
|
||
| function sendRequest(request: unknown): Promise<void> { |
There was a problem hiding this comment.
🟡 Medium builtin/herdr-agent-state.ts:161
sendRequest() resolves on the first byte of response data or after a fixed 500ms timeout, so releaseAgent() can proceed to send pane.release_agent while an earlier pane.report_agent is still in-flight on the Herdr side. When Herdr is slow to process replies, the earlier report lands after the release and reclaims the pane, leaving Herdr showing prime-agent as attached after it has exited. The await activeDrain in releaseAgent() only guarantees the socket write completed, not that Herdr finished processing the report. Consider waiting for a full reply (or an explicit ack) before resolving sendRequest(), or draining pending reports with a stronger completion guarantee before sending the release.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts around line 161:
`sendRequest()` resolves on the first byte of response data or after a fixed 500ms timeout, so `releaseAgent()` can proceed to send `pane.release_agent` while an earlier `pane.report_agent` is still in-flight on the Herdr side. When Herdr is slow to process replies, the earlier report lands after the release and reclaims the pane, leaving Herdr showing `prime-agent` as attached after it has exited. The `await activeDrain` in `releaseAgent()` only guarantees the socket write completed, not that Herdr finished processing the report. Consider waiting for a full reply (or an explicit ack) before resolving `sendRequest()`, or draining pending reports with a stronger completion guarantee before sending the release.
There was a problem hiding this comment.
Declining. Herdr's API handler applies the request before replying (its api.request.complete log lines follow the state change), so a pane.report_agent ACK means the report is already applied server-side. releaseAgent awaits the active drain — which resolves only on the report's ACK — before sending the release, so the release cannot be outrun by an already-ACKed report. The 500ms timeout path only fires when herdr never replies, in which case that connection is destroyed client-side and its late processing is the same failure mode as any dropped write; the released flag prevents any further reports after it.
…ern subset The agent's auto-retry treats nearly all provider errors as retryable and runs after extension agent_end, so an error the reporter's pattern list missed flipped the pane to idle while a retry was pending. Drop the pattern list and hold working for any error end; a retry's agent_start keeps the pane working, and with no retry the grace window settles it to blocked with the error message.
…utdown A replaced instance kept its report queue armed after reload/new/resume shutdowns; a late report carrying the process-wide seq could outrank and stomp the successor's state. Non-quit shutdowns now set released and drop the queued state so the old instance goes quiet.
noExtensions is a full opt-out: inline factories deliberately bypass it for embedder-supplied extensions, but the built-in reporter should respect it so --no-extensions leaves nothing extension-driven running.
…t loaded Deferral now consults the resource loader's actually-loaded extension paths (late-bound, per factory invocation) instead of raw disk existence, so a herdr-agent-state file disabled via settings overrides or outside the discovery dirs no longer silences the built-in reporter and leaves the pane with no reporter.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 07bf7bd. Configure here.
| // Set before inline factories run so a factory can see which file-based | ||
| // extensions actually loaded this cycle (e.g. the built-in Herdr reporter | ||
| // defers to Herdr's own file-based integration only when it is active). | ||
| this.loadedExtensionPaths = extensionPaths; |
There was a problem hiding this comment.
Deferral uses unloaded extension paths
Medium Severity
getLoadedExtensionPaths() is assigned the candidate extensionPaths list, not the extensions that successfully loaded. If herdr-agent-state.ts/js is discovered but fails to load, hasFileBasedHerdrIntegration still returns true and the built-in reporter no-ops, leaving the pane with no active Herdr reporter.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 07bf7bd. Configure here.
…f-windows Fix edit tool failing on Windows due to CRLF line endings
* feat(coding-agent): add built-in herdr integration Herdr cannot detect Prime Agent panes: its process detection only knows the legacy "pi" binary name, and its "herdr integration install pi" writes to ~/.pi/agent/extensions which Prime Agent does not read. Ship the pi lifecycle-reporter as a built-in extension factory loaded for every session. It reports working/idle/blocked and session refs to Herdr's unix socket and releases the pane on shutdown, using the agent label "prime-agent" (verified accepted and displayed by herdr 0.7.0 against a live pane). The factory captures HERDR_* env per invocation, so daemon sessions pick up their own pane identity inside the client-env window, and it is a no-op outside Herdr or when the user has installed Herdr's own file-based integration. * fix(coding-agent): keep herdr seq monotonic across sessions and release only on quit Herdr guards pane.report_agent with a per-source monotonic seq and silently drops lower-seq reports (verified live: ok response, state unchanged). Seeding seq per factory instance meant a successor session after /new, resume, fork, or reload could restart below the predecessor's counter, so all its reports were dropped and the pane stuck at working. - move the seq counter to module scope and clamp it to max(prev + 1, now*1000) so it never regresses across instances - release the pane only on quit; on session replacement the successor re-reports immediately and a racing release could clear it - report idle immediately on agent_end when no messages are queued; keep the debounce only when queued follow-ups restart the loop * fix(coding-agent): address herdr integration review findings - unsubscribe the shared-bus herdr:blocked listener on session_shutdown; the event bus survives reloads and session replacements, so a leaked listener kept stale instances reporting with captured pane identity - move the file-based integration check into createAgentSessionServices using the same extension dirs the loader discovers from, so agentDir overrides are honored - stop treating the legacy ~/.pi/agent path as an active reporter; Prime Agent never loads extensions from there, so deferring to it left panes with no reporter at all * fix(coding-agent): scope herdr reporter to its own session and recheck deferral on reload - bind the reporter to the first session that starts and ignore events from other sessions sharing the closure; inline RLM children reuse the parent's resource loader, so a subagent's turns flipped the pane state and a subagent quit released the pane while the parent was running - skip the built-in reporter entirely for subagent runtimes (rlmDepth > 0), which share the parent's HERDR_* pane identity - re-check for Herdr's file-based integration on every factory invocation instead of once at service creation, so installing it and running /reload hands the pane over without both reporters active - also match herdr-agent-state.js for compiled installs * fix(coding-agent): make quit release the final write to the herdr socket releaseAgent now stops new reports, drops the queued state, and awaits the in-flight drain before sending pane.release_agent, so a late report cannot land after the release and reclaim the pane for an exited agent. * fix(coding-agent): seed working state when the reporter starts mid-turn Daemon-driven reloads and extension ctx.reload() are not gated on idle, so a fresh reporter's session_start could publish idle while the agent was still streaming, and the agentActive guard then swallowed the turn's real end transition. session_start now seeds agentActive from ctx.isIdle(). * fix(coding-agent): settle the retry hold when a blocked event cancels its timer A herdr:blocked event during the retry-hold window cancelled the retry timer but left retryHoldActive set, so after the block lifted the pane reported working forever. The blocked handler now settles the hold to failureBlocked, matching what the cancelled timer would have done. * fix(coding-agent): do not defer to file-based herdr reporter under noExtensions With noExtensions the loader never scans the extension dirs, so a file-based herdr-agent-state there never becomes an active reporter; deferring to it left the pane with no reporter at all. * fix(coding-agent): hold working for every error end instead of a pattern subset The agent's auto-retry treats nearly all provider errors as retryable and runs after extension agent_end, so an error the reporter's pattern list missed flipped the pane to idle while a retry was pending. Drop the pattern list and hold working for any error end; a retry's agent_start keeps the pane working, and with no retry the grace window settles it to blocked with the error message. * fix(coding-agent): silence replaced reporter instances on non-quit shutdown A replaced instance kept its report queue armed after reload/new/resume shutdowns; a late report carrying the process-wide seq could outrank and stomp the successor's state. Non-quit shutdowns now set released and drop the queued state so the old instance goes quiet. * fix(coding-agent): make noExtensions disable the built-in herdr reporter noExtensions is a full opt-out: inline factories deliberately bypass it for embedder-supplied extensions, but the built-in reporter should respect it so --no-extensions leaves nothing extension-driven running. * fix(coding-agent): defer to the file-based herdr reporter only when it loaded Deferral now consults the resource loader's actually-loaded extension paths (late-bound, per factory invocation) instead of raw disk existence, so a herdr-agent-state file disabled via settings overrides or outside the discovery dirs no longer silences the built-in reporter and leaves the pane with no reporter.


Summary
Herdr does not detect Prime Agent panes today, for two reasons:
pibinary name, notprime-agent(upstream PR Add Prime Agent support for Pi integration herdrdev/herdr#822 to add the alias was auto-closed unmerged).herdr integration install piwrites its lifecycle-reporter extension to~/.pi/agent/extensions/, which Prime Agent does not read (~/.prime/agent/).This ships the lifecycle reporter as a built-in extension factory loaded for every session, so Prime Agent works inside Herdr panes out of the box with no manual install step and no dependency on upstream Herdr changes.
Behavior
working/idle/blockedplus session file references to Herdr's unix socket viapane.report_agent, and releases the pane onsession_shutdown.prime-agent. Verified live against herdr 0.7.0: the label is accepted, and the pane shows"agent": "prime-agent"inherdr pane list/herdr agent listwith correct working/idle transitions and release.HERDR_ENV=1withHERDR_SOCKET_PATHandHERDR_PANE_IDset, so nothing changes outside Herdr.herdr-agent-state.tsexists in the extensions dir (either~/.prime/agent/or legacy~/.pi/agent/), so the two reporters never race on one pane.Testing
test/herdr-agent-state.test.ts: fake Herdr socket server asserting the report/release protocol, agent label, session refs, and both no-op paths (outside Herdr; file-based integration present).prime-agent, state transitions, release).npm run checkpasses.Note
Medium Risk
Touches daemon session lifecycle and external Herdr IPC on every session load; logic is intricate but gated off Herdr env and heavily tested.
Overview
Adds a built-in Herdr integration so Prime Agent reports working / idle / blocked (and session file refs) to Herdr’s Unix socket as
prime-agent, withoutherdr integration install pi.The reporter is registered as a default inline extension factory in
createAgentSessionServices. It is a no-op unlessHERDR_ENV=1with socket and pane id; it defers when a loaded file extension namedherdr-agent-state.ts/jsis active (via newgetLoadedExtensionPaths()), and is disabled for RLM subagents (noBuiltinHerdrReporterwhenrlmDepth > 0) and whennoExtensionsis set.The implementation handles queued socket writes, monotonic seq across reloads, retry/error hold before blocked, idle debounce when follow-ups are queued, parent-only binding for inline RLM children, release only on quit (not on session replace/reload), and
herdr:blockedbus events. Covered byherdr-agent-state.test.ts.Reviewed by Cursor Bugbot for commit 07bf7bd. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add built-in Herdr integration to report agent lifecycle state
herdr-agent-state.tsthat connects via Unix socket whenHERDR_ENV=1and reportsworking/blocked/idlestates to the configured Herdr pane.createAgentSessionServiceswithout requiring manual installation; it defers to any file-based Herdr integration that loaded in the same cycle.rlmDepth > 0) opt out vianoBuiltinHerdrReporter=trueto avoid racing or prematurely releasing the parent pane.workingstate across retryable errors, and a monotonically increasing sequence number for Herdr's per-source ordering requirement.DefaultResourceLoadernow exposesgetLoadedExtensionPaths()so inline factories can inspect which file-based extensions loaded in the current cycle.Macroscope summarized 07bf7bd.